Pandas Tutorial: Core Concepts
pandas is the most widely used Python library for data manipulation and analysis. It provides fast, flexible data structures (DataFrame and Series) designed to work with structured (tabular) data and time series.
In this tutorial, we'll cover the core concepts of pandas, from installation through to advanced operations and a capstone project.
📥 Download the Sample Data
All the datasets used in this tutorial are available for download. Save them to your working directory to follow along.
| File | Description | Link |
|---|---|---|
sales_data.csv | Sales transactions for the capstone project (~500 rows) | Download |
employees.csv | Employee records for merge/join examples (20 rows) | Download |
departments.csv | Department data for merge/join examples (5 rows) | Download |
weather_data.csv | Daily weather time series (2 years) | Download |
student_scores.csv | Student scores for reshape/pivot examples (30 rows) | Download |
💡 Tip: Place all files in the same directory as your notebook or script, then use the filenames directly in
pd.read_csv().
1. Introduction to pandas
Key Features
- DataFrame — a 2D labeled data structure (like a spreadsheet or SQL table)
- Series — a 1D labeled array (like a single column)
- Vectorized operations — apply operations to entire arrays without explicit loops
- Built-in I/O — read/write CSV, Excel, JSON, SQL, Parquet, and more
- Time series support — date parsing, resampling, rolling windows
- Integration — works seamlessly with NumPy, Matplotlib, scikit-learn
Installation
pip install pandas
Import Convention
import pandas as pd
import numpy as np
2. Core Data Structures
Series — 1D Labeled Array
# From a list
s = pd.Series([10, 20, 30, 40])
# With a custom index
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
# From a dictionary (keys become the index)
s = pd.Series({"x": 100, "y": 200, "z": 300})
DataFrame — 2D Table
# From a dictionary of lists
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
"city": ["Lagos", "Accra", "Nairobi"],
})
# From a list of dicts
df = pd.DataFrame([
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
])
Inspecting a DataFrame
df.shape # (rows, columns)
df.dtypes # data type of each column
df.columns # column labels
df.info() # concise summary: dtypes, non-null counts, memory use
df.describe() # statistics for numeric columns
df.head() # first 5 rows
df.tail() # last 5 rows
df.sample(5) # 5 random rows
3. Indexing and Selection
.loc — Label-Based
df.loc["a"] # single row by label
df.loc[["a", "c"]] # multiple rows
df.loc["a":"c"] # slice (inclusive)
df.loc["b", "age"] # single cell
df.loc[:, "age"] # all rows, one column
.iloc — Position-Based
df.iloc[0] # first row
df.iloc[-1] # last row
df.iloc[0:2] # first two rows (end-exclusive)
df.iloc[0, 1] # cell at row 0, column 1
df.iloc[:, 2] # all rows, column at position 2
Boolean Filtering
df[df["age"] > 30]
df[(df["age"] > 25) & (df["city"] == "Accra")]
df[df["name"].str.startswith("A")]
df[df["city"].str.contains("ro")]
.query() — Readable Filtering
df.query("age > 30")
df.query("age > 25 and city == 'Accra'")
min_age = 30
df.query("age >= @min_age")
Setting the Index
df.set_index("name", inplace=True)
df.reset_index(inplace=True)
Fast Scalar Access
df.at["b", "age"] # label-based scalar
df.iat[1, 1] # position-based scalar
4. Data Cleaning
Missing Values
df.isnull().sum() # count NaN per column
df.dropna() # drop rows with any NaN
df.dropna(subset=["age", "salary"]) # drop rows where specific columns are NaN
df.fillna(0) # replace all NaN with 0
df["age"].fillna(df["age"].mean()) # fill with column mean
df.fillna(method="ffill") # forward fill
df["price"].interpolate() # linear interpolation
Duplicates
df.duplicated().sum()
df.drop_duplicates()
df.drop_duplicates(subset=["email"])
df.drop_duplicates(keep="last")
Data Types
df["age"] = df["age"].astype(int)
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = pd.to_numeric(df["revenue"], errors="coerce")
df["country"] = df["country"].astype("category")
String Cleaning
df["name"] = df["name"].str.strip().str.lower()
df["phone"] = df["phone"].str.replace("-", "", regex=False)
df[["first", "last"]] = df["name"].str.split(" ", expand=True)
5. Transforming Data
Column Arithmetic
df["profit"] = df["revenue"] - df["cost"]
df["margin_pct"] = (df["profit"] / df["revenue"]) * 100
apply, map, assign
df["age_group"] = df["age"].apply(lambda x: "Young" if x < 30 else "Senior")
df["country_code"] = df["country"].map({"Nigeria": "NG", "Ghana": "GH"})
df = df.assign(
profit=lambda x: x["revenue"] - x["cost"],
year=lambda x: x["date"].dt.year,
)
Binning
df["age_bin"] = pd.cut(df["age"], bins=[0, 18, 35, 60, 100],
labels=["Child", "Young Adult", "Adult", "Senior"])
df["score_quartile"] = pd.qcut(df["score"], q=4, labels=["Q1", "Q2", "Q3", "Q4"])
Conditional Logic
import numpy as np
df["flag"] = np.where(df["score"] >= 80, "pass", "fail")
conditions = [df["score"] >= 90, df["score"] >= 75, df["score"] >= 60]
choices = ["A", "B", "C"]
df["grade"] = np.select(conditions, choices, default="F")
6. Aggregation and Grouping
Basic GroupBy
df.groupby("dept")["salary"].mean()
df.groupby(["dept", "years"])["salary"].sum()
Multiple Aggregations
df.groupby("dept")["salary"].agg(["min", "max", "mean", "count"])
df.groupby("dept").agg(
avg_salary=("salary", "mean"),
headcount=("name", "count"),
)
Transform
df["dept_avg_salary"] = df.groupby("dept")["salary"].transform("mean")
Filter Groups
df.groupby("dept").filter(lambda g: len(g) > 1)
7. Merging and Joining
pd.concat — Stacking
combined = pd.concat([df1, df2], ignore_index=True)
pd.concat([df_a, df_b], axis=1) # horizontal stacking
pd.merge — SQL-style Joins
pd.merge(employees, departments, on="dept_id") # inner (default)
pd.merge(employees, departments, on="dept_id", how="left")
pd.merge(employees, departments, on="dept_id", how="right")
pd.merge(employees, departments, on="dept_id", how="outer")
pd.merge(orders, customers, left_on="customer_id", right_on="id")
DataFrame.join — Index-based
df1.join(df2) # left join on index
8. Reshaping Data
Pivot (Long → Wide)
wide = df.pivot(index="date", columns="product", values="sales")
df.pivot_table(index="date", columns="product", values="sales", aggfunc="sum")
Melt (Wide → Long)
long = wide.melt(id_vars="name", var_name="subject", value_name="score")
Stack / Unstack
stacked = df.stack()
stacked.unstack()
Explode
df.explode("tags") # one row per list element
9. Time Series
Parsing Dates
df["date"] = pd.to_datetime(df["date"])
df["date"] = pd.to_datetime(df["date"], format="%d/%m/%Y")
DatetimeIndex
df.set_index("date", inplace=True)
df["2024"] # all rows in 2024
df["2024-03"] # all rows in March 2024
.dt Accessor
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["dow"] = df["date"].dt.day_name()
Resampling
df.resample("M")["revenue"].sum() # monthly sum
df.resample("W")["price"].mean() # weekly mean
df.resample("D").ffill() # upsample & forward-fill
Rolling Windows
df["7d_avg"] = df["revenue"].rolling(window=7).mean()
df["30d_sum"] = df["revenue"].rolling(window=30).sum()
Shifting
df["prev_day"] = df["revenue"].shift(1)
df["change"] = df["revenue"].diff(1)
df["pct_chg"] = df["revenue"].pct_change(1)
10. Input / Output
CSV
df = pd.read_csv("data.csv", parse_dates=["date"], usecols=["name", "age"])
df.to_csv("output.csv", index=False)
Excel
df = pd.read_excel("data.xlsx", sheet_name="Sales")
df.to_excel("output.xlsx", index=False)
JSON
df = pd.read_json("data.json")
df.to_json("output.json", orient="records", indent=2)
SQL
from sqlalchemy import create_engine
engine = create_engine("postgresql://user:pass@localhost/mydb")
df = pd.read_sql("SELECT * FROM orders", engine)
df.to_sql("orders_clean", engine, if_exists="replace", index=False)
Parquet
df.to_parquet("data.parquet", index=False)
df = pd.read_parquet("data.parquet")
11. Performance Best Practices
Choose the Right dtypes
df["age"] = pd.to_numeric(df["age"], downcast="integer")
df["country"] = df["country"].astype("category")
Avoid Loops — Use Vectorized Operations
# Bad
for i, row in df.iterrows():
df.at[i, "tax"] = row["salary"] * 0.15
# Good
df["tax"] = df["salary"] * 0.15
Method Chaining
result = (
pd.read_csv("data.csv")
.dropna(subset=["revenue"])
.assign(year=lambda x: x["date"].dt.year)
.query("year >= 2023")
.groupby("region")["revenue"]
.sum()
.reset_index()
)
Read Only What You Need
pd.read_csv("data.csv", usecols=["name", "salary"], nrows=10_000)
12. Visualization
Built-in Plotting
df["revenue"].plot(title="Revenue", figsize=(10, 4))
df.groupby("region")["revenue"].sum().plot(kind="bar")
df["age"].plot(kind="hist", bins=20, edgecolor="black")
df.plot(kind="scatter", x="age", y="salary")
Seaborn
import seaborn as sns
sns.histplot(data=df, x="age", kde=True)
sns.boxplot(data=df, x="dept", y="salary")
sns.heatmap(df.corr(numeric_only=True), annot=True)
Saving Figures
plt.savefig("chart.png", dpi=150, bbox_inches="tight")
13. Capstone Project
This end-to-end project applies everything you've learned.
Load and Inspect
df = pd.read_csv("sales_data.csv")
print(df.shape, df.dtypes, df.head())
Clean
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["order_id", "date", "quantity"])
df = df.drop_duplicates(subset=["order_id"])
df["region"] = df["region"].str.strip().str.title()
df["category"] = df["category"].astype("category")
Transform
df["revenue"] = df["quantity"] * df["unit_price"] * (1 - df["discount"])
df["profit"] = df["revenue"] - df["cost"]
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.to_period("M")
Aggregate
by_region = df.groupby("region").agg(
total_revenue=("revenue", "sum"),
total_profit=("profit", "sum"),
order_count=("order_id", "count"),
).sort_values("total_revenue", ascending=False)
Visualize
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
by_region["total_revenue"].plot(kind="barh", ax=axes[0, 1], title="Revenue by Region")
df.groupby("month")["revenue"].sum().plot(ax=axes[0, 0], title="Monthly Revenue")
plt.tight_layout()
plt.savefig("dashboard.png", dpi=150)
plt.show()
Summary
print(f"Total Revenue: ${df['revenue'].sum():,.0f}")
print(f"Total Profit: ${df['profit'].sum():,.0f}")
print(f"Total Orders: {df['order_id'].nunique():,}")
Summary
pandas is the essential tool for data manipulation in Python. Here's a recap of the key concepts we covered:
pd.Series()/pd.DataFrame(): Core data structures.loc/.iloc: Label and position-based selection- Boolean filtering /
.query(): Conditional row selection fillna()/dropna(): Handle missing valuesgroupby()/agg(): Group and summarise datapd.merge()/pd.concat(): Combine DataFramespivot()/melt(): Reshape between wide and long formatsresample()/rolling(): Time series operationspd.read_csv()/to_csv(): Input and output.plot(): Quick visualization